package com.erdos.message;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Session;
import javax.naming.Context;
import javax.naming.NamingException;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.erdos.common.cache.CacheManager;
import com.erdos.common.cache.Cacheable;
import com.erdos.common.util.JndiUtil;

import flex.messaging.MessageException;

public class JmsSessionLocator implements Cacheable {
	private static final Log logger = LogFactory.getLog(JmsSessionLocator.class);
	private static final String JNDI_FILE="jms.properties";
	private static final Configuration config=getConfiguration();
	
	private static final String DEFAULT_JMS_CONN_FACTORY_KEY="jms.connection.factory.name";
	private static final String DEFAULT_JMS_CONN_FACTORY_NAME="GenericConnectionFactory";
	private static final String DEFAULT_JMS_CONN_FACTORY_CLASS="jms.connection.factory.class";
	
	private static final String SENDER_PREFIX_KEY="sender_prefix";	
	private static final String TARGET_APPLICATION="target_application";
	
	private static final Map<String, Context> contextMap=getContextMap();
	private static final String DEFAULT_CONTEXT="";
	/**
	 * cache the already found ConnectionFactory through JNDI for quick access.<br>
	 * key: jndi name of connection factory<br>
	 * value: jms ConnectionFactory
	 */
//	private static final Map<String, ConnectionFactory> jmsConnFactoryCache=new HashMap<String, ConnectionFactory>();
	/**
	 * jms connection cache for quick access.<br>
	 * key: conntion factory name:client id<br>
	 * value: jms Connection
	 */
//	private static final Map<String, Connection> jmsConnectionCache=new HashMap<String,Connection>();
	
	/**
	 * cache the already found Destination through JNDI for quick access.
	 */
//	private static final Map<String, Destination> channelCache=new HashMap<String, Destination>();	
	
	private static final String CACHE_DEST_PREFIX="JMS:DEST:";
	private static final String CACHE_CONN_PREFIX="JMS:CONN:";
	private static final String CACHE_CONNFACTORY_PREFIX="JMS:CONNFACTORY:";

	
	/**
	 * locates the Jms Connection Factory through the specified Jndi name.
	 * @param jndiName Jndi name of the Jms connection factory 
	 * @return a Jms ConnectionFactory with the specified Jndi <code>jndiName</code> 
	 */
	private static ConnectionFactory locateConnectionFactory(String jndiName) {
		if(logger.isDebugEnabled()) {
			logger.debug("locateConnectionFactory: " + jndiName);
		}
		ConnectionFactory connFactory=(ConnectionFactory)CacheManager.getFromCache(CACHE_CONNFACTORY_PREFIX+jndiName);
		
		if(connFactory ==null) {
			try{				
				connFactory=(ConnectionFactory)getJndiObject(jndiName);
				if(connFactory !=null) {
					CacheManager.putInCache(CACHE_CONNFACTORY_PREFIX+jndiName, connFactory);
				}				
			} catch (Exception ex) {	
				String factoryClass=getEnv(jndiName, DEFAULT_JMS_CONN_FACTORY_CLASS);
				logger.warn("jndiName not found: " + jndiName+". " + ex.getMessage() + " .try to initialize the connection factory class: " + factoryClass);
/*				try{	
					connFactory=(ConnectionFactory)Class.forName(factoryClass).newInstance();
					
					 // for IBM Websphere MQ. do NOT work yet.
					// the below code can be refactored to an factory pattern class to create the connection factory
					 
					if(connFactory instanceof MQConnectionFactory) {
						MQConnectionFactory mqCF=(MQConnectionFactory) connFactory;
						String queueMgr=getEnv(jndiName, "queueManager");
						mqCF.setQueueManager(queueMgr);
						mqCF.setHostName(getEnv(jndiName, "hostName"));
						int port=Integer.valueOf(getEnv(jndiName, "port"));
						mqCF.setPort(port);
						int transType=Integer.valueOf(getEnv(jndiName, "transportType"));
						mqCF.setTransportType(transType);
						mqCF.setChannel(getEnv(jndiName, "channel"));
						int ccsid=Integer.valueOf(getEnv(jndiName, "ccsid"));
						mqCF.setCCSID(ccsid);
					}
					CacheManager.putInCache(CACHE_CONNFACTORY_PREFIX+jndiName, connFactory);*/
					
//				} catch (Exception ex2) {
//					logger.warn("try to initialize the connection factory class " + jndiName + ", but failed.");
//					throw new IllegalArgumentException(ex);
//				}				
			}			
		}
	      if(logger.isDebugEnabled()) {
	          if(connFactory==null) {
	              logger.debug(" failed to locate ConnectionFactory: " + jndiName);
	          } else {
	              logger.debug("successfully located ConnectionFactory: " + jndiName);
	          }	             
	        }
		return connFactory;
	}
	
	private static String[] getConnectionParam(String jndiName) {
		String [] connStr=new String[]{"", jndiName};
		if(jndiName!=null && jndiName.indexOf(":")>0) {
			connStr=StringUtils.split(jndiName, ":", 2);
		} 
		
		if(logger.isDebugEnabled()) {
			logger.debug("connection parameter: " + jndiName + " separeted into " + Arrays.asList(connStr));
		}
		
		return connStr;
	}
	
	/**
	 * gets a Jms Connection with the default pre-configured Jms Connection factory.
	 * @return a Jms Connection
	 */
	public  static Connection getDefaultJmsConnection(String clientId) {
		if(logger.isDebugEnabled()) {
			logger.debug("getDefaultJmsConnection: " + clientId);
		}
		String jmsConnFactoryName=getDefaultJmsConnFactoryName(clientId);
		return getJmsConnection(jmsConnFactoryName, clientId);
	}
	
	/**
	 * gets the Jms Session with the default pre-configured Jms connection factory.
	 * @param transacted is this returned Session transacted
	 * @param acknowledgeMode the acknowledgeMode of this Session
	 * @return a Jms Session
	 */
	public static Session getDefaultJmsSession(String destination, boolean transacted, int acknowledgeMode) {
		String jmsConnFactoryName=getDefaultJmsConnFactoryName(destination);
		return getJmsSession(jmsConnFactoryName, destination, transacted, acknowledgeMode);
	}
	/**
	 * supports multiple jms server to get the connection factory name.
	 * @param clientId format: prefix:clientName or clientName, prefix is used to identify the jms server
	 * @return a jms connection factory name which supports multiple jms server
	 */
	private static String getDefaultJmsConnFactoryName(String clientId) {
		String prefix=getConnectionParam(clientId)[0];
		String factoryName=getEnv(prefix, DEFAULT_JMS_CONN_FACTORY_KEY);
//		if(StringUtils.isNotBlank(prefix)) {
//			factoryName=config.subset(prefix).getString(DEFAULT_JMS_CONN_FACTORY_KEY);
//		} else {
//			factoryName=config.getString(DEFAULT_JMS_CONN_FACTORY_KEY);
//		}
		return StringUtils.isBlank(factoryName)?DEFAULT_JMS_CONN_FACTORY_NAME:factoryName;
	}
	
	/**
	 * creates a new JMS <code>Session</code> with the specified parameters.
	 * @param jmsConnFactoryName jndi of the to be found ConnectionFactory
	 * @param destination which mq server to connect to
	 * @param transacted is this returned Session transacted
	 * @param acknowledgeMode the acknowledgeMode of this Session
	 * @return a newly create JMS Session
	 */
	public static Session getJmsSession(String jmsConnFactoryName, String destination, boolean transacted, int acknowledgeMode) {		
		if(logger.isDebugEnabled()) {
			logger.debug("getJmsSession: factoryName=" + jmsConnFactoryName + ", destination=" +destination 
					+ ",transacted=" + transacted+ ",acknowledgeMode="+acknowledgeMode);
		}
		Session session=null;
		try {
			session=getJmsConnection(jmsConnFactoryName, destination).createSession(transacted, acknowledgeMode);
		} catch (JMSException ex) {
			String key=destination + ":" + jmsConnFactoryName;
			CacheManager.removeEntry(CACHE_CONN_PREFIX+key);
			try{
				session=getJmsConnection(jmsConnFactoryName, destination).createSession(transacted, acknowledgeMode);
			} catch (JMSException ex2) {
				logger.warn("jmsConnFactoryName: " + jmsConnFactoryName + ", destination: " + destination, ex);
				throw new MessageException(ex);
			}			
		}
		return session;
	}
	
	/**
	 * cache the jms connection.
	 * @param jmsConnFactoryName
	 * @return Jms Connection
	 */
	public static Connection getJmsConnection(String jmsConnFactoryName, String clientId) {
		if(logger.isDebugEnabled()) {
			logger.debug("getJmsConnection: factoryName=" + jmsConnFactoryName + ", destination=" + clientId);
		}
		long start=System.currentTimeMillis();
		String server="";
		if(StringUtils.contains(clientId,  ":")) {
			server=StringUtils.substringBefore(clientId, ":");
		}
		String serverConnFactoryName=StringUtils.isEmpty(server)?jmsConnFactoryName:server+":" + jmsConnFactoryName;
		
		ConnectionFactory connFactory=locateConnectionFactory(serverConnFactoryName);
		
		String key=clientId + ":" + jmsConnFactoryName;
		Connection conn=(Connection)CacheManager.getFromCache(CACHE_CONN_PREFIX+key);
		try {
			if(connFactory !=null && conn ==null) {				
				conn=connFactory.createConnection();
				
				setConnectionClientId(conn, key);
				
				CacheManager.putInCache(CACHE_CONN_PREFIX+key, conn);	
			}
		} catch (JMSException ex) {
			logger.warn("failed to get Connection: jmsConnFactoryName: " + jmsConnFactoryName + ", destination: " + clientId + ", duration=" +(System.currentTimeMillis()-start) + " millis");
			throw new MessageException(ex);
		}
		
		if(logger.isDebugEnabled()) {
		    if(conn==null) {
		        logger.debug("failed to got Connection : jmsConnFactoryName: " + jmsConnFactoryName + ", destination: " + clientId + ", duration=" + (System.currentTimeMillis()-start) + " millis");
		    } else {
		        logger.debug("successfully got Connection : jmsConnFactoryName: " + jmsConnFactoryName + ", destination: " + clientId + ", duration=" + (System.currentTimeMillis()-start) + " millis");
		    }			
		}
		return conn;
	}
	
	private static Context getContext(String jndiName) {
		String prefix=DEFAULT_CONTEXT;
		if(StringUtils.contains(jndiName, ":") ) {
			String [] connStr=getConnectionParam(jndiName);
			prefix=connStr[0];
		} 
		
		Context ctx=contextMap.get(prefix);
		if(ctx==null && !DEFAULT_CONTEXT.equals(prefix)) {
			ctx=contextMap.get(DEFAULT_CONTEXT);
			if(logger.isDebugEnabled()) {
				logger.debug("context " + prefix + " is not found, use the default context instead");
			}			
		}
		return ctx;
	}
	
	private static Object getJndiObject(String jndiName) throws NamingException {		
		Context ctx=getContext(jndiName);
		String [] connStr=getConnectionParam(jndiName);
		return ctx==null?null:JndiUtil.locate(ctx, connStr[1]);
	}
	
	public static Destination locateDestination(String jndiName) {
		if(StringUtils.isBlank(jndiName)) {
			return null;
		}
		long start=System.currentTimeMillis();
		jndiName=StringUtils.trim(jndiName);
		Destination dest=(Destination)CacheManager.getFromCache(CACHE_DEST_PREFIX + jndiName);
		
		if(dest ==null) {
			try{
				dest=(Destination)getJndiObject(jndiName);
				
				if(dest !=null) {
					CacheManager.putInCache(CACHE_DEST_PREFIX + jndiName, dest);
				}				
			} catch (Exception ex) {
				logger.debug("failed to locate Destination: " + jndiName + ", duration=" + (System.currentTimeMillis()-start) + " millis");
								
				throw new IllegalArgumentException(ex.getMessage());
			}
			
		}
		
		if(logger.isDebugEnabled()) {
		    if(dest !=null) {
		        logger.debug("successfully located Destination: " + jndiName + ", duration=" + (System.currentTimeMillis()-start) + " millis");
		    } else {
		        logger.debug("failed to locate Destination: " + jndiName + ", duration=" + (System.currentTimeMillis()-start) + " millis");
		    }			
		}
		return dest;
	}
	
	private synchronized static void setConnectionClientId(Connection conn, String clientId) {
		try {
			if(!CacheManager.isCacheKeyExisting(CACHE_CONN_PREFIX+clientId) && conn.getClientID()==null) {
				conn.setClientID(clientId);			
			}
		} catch (JMSException ex) {
			logger.warn("client Id: " + clientId + ": " + ex.getMessage());
		}
	}
	
	public static void closeConnection(String key) {
		String cacheKey="";
		try {
			Connection conn=null;
			String server=null;
			String connFactoryName=null;
			
			if( StringUtils.contains(key, ":")) {
				cacheKey=CACHE_CONN_PREFIX + key;
				conn=(Connection)CacheManager.getFromCache(cacheKey);
				if(conn==null) {
					server=StringUtils.substringBefore(key, ":") + ":";
					connFactoryName=getDefaultJmsConnFactoryName(key);
				}
			} else {
				server=key +":";
				connFactoryName=getDefaultJmsConnFactoryName(key);
			}
			
			if(conn==null) {
				cacheKey=CACHE_CONN_PREFIX + (server+connFactoryName) ;
				conn=(Connection)CacheManager.getFromCache(cacheKey);
			}
			CacheManager.removeEntry(cacheKey);
			
			if(conn !=null) {				
				try {
					conn.stop();
				} catch (Exception e1) {
					logger.warn("failed to stop the connection." + cacheKey);
				} finally {
					conn.close();
				}								
			}
		} catch (Exception ex) {
			logger.warn("failed to close connection. id: " + cacheKey, ex);
		}
	}
	
	public static String getSenderPrefix() {
		return config.getString(SENDER_PREFIX_KEY, "SENDER");
	}
	
	/**
	 * returns the application for this Jms targets to.
	 * the property is defined in JNDI_FILE.
	 */
	public static String getTargetApplication() {
		return config.getString(TARGET_APPLICATION, "");
	}
	
	/**
	 * returns the server which this Destination name connects to.
	 * eg. if destination=abc, then returns the default jms connection server,<br>
	 * if destination=server:abc, it returns the jms server which defined with the prefix server.<br>
	 * if nothing found, returns empty string.
	 * @param destination the jms destination defined in system
	 * @return a String indicates this destination connects to
	 */
	public static String getServerName(String destination) {
		String serverName="";
		try{
			Context ctx=getContext(destination);
			if(ctx !=null) {
				serverName=(String)ctx.getEnvironment().get(Context.PROVIDER_URL);
			}
		} catch (NamingException nex) {
			logger.warn("failed to get the server name through" + destination, nex);
		}
		return serverName;
	}
	
	private static Configuration getConfiguration() {
		return JndiUtil.getConfiguration(JNDI_FILE);
	}
	
	private static Map<String,Context> getContextMap() {
		long start=System.currentTimeMillis();
		Map<String,Context> ctxMap=new HashMap<String, Context>();
		try{
			ctxMap=JndiUtil.getInitialContextMap(config);
		} catch (Exception ex) {
			logger.error("Jndi file: " + JNDI_FILE, ex);
		}
		logger.info("initialize context map duration: " + (System.currentTimeMillis()-start) + " mills");
		return ctxMap;
	}

	/**
	 * combines prefix and key together to get the specified value.
	 * e.g. prefix=abc.key1 key=key1 <br>
	 * then it will locate context of abc, and lookup the key2.
	 * 
	 * @param prefix prefix string
	 * @param key to be lookup key in the context
	 * @return a string value of the specified key
	 */
	private static String getEnv(String prefix, String key) {
		Context ctx=getContext(prefix + ":" + key);
		String value=null;
		if(ctx !=null) {			
			try {
				value = (String)ctx.getEnvironment().get(key);
			} catch (NamingException e) {
				logger.warn("failed to get property. prefix="+prefix +", key=" + key);
			}
		}
		return value;
	}
	
    /**
     * flushes the cached Destination from the cache.
     * @param dest key of the  to-be removed Destination from cache
     */
    public static void refreshDestination(String dest) {
        if(StringUtils.isNotBlank(dest)) {
            CacheManager.removeEntry(CACHE_DEST_PREFIX + StringUtils.trim(dest));
         }
    }
    
	/**
	 * refresh the Jms connection factory, Jms connection and Jms Destination.
	 */
	public void refresh() {
		contextMap.clear();	
		getConfiguration();
		getContextMap();
	}
	
}
